#!/bin/bash

# When running with "-v", the test itself runs in a pipeline with tee, and
# without pipefail we get the exit value from tee instead of from the test.
set -o pipefail

# The linuxcnc starter script sometimes tries to display X windows if
# DISPLAY is set.  We never want that while running tests, so unset it.
unset DISPLAY

# Some of our tests emit locale-sensitive strings, so reset the locale
# to a sane default.
export LC_ALL=C
export LANGUAGES=

TOPDIR="$(realpath "$(dirname "$0")/..")"

# $prefix is used in the substitutions of @...@ strings below
# shellcheck disable=SC2034
prefix=@prefix@
# Shellcheck does not know about substitutions
# shellcheck disable=SC2050
if [ "@RUN_IN_PLACE@" = yes ]; then
    . "$TOPDIR/scripts/rip-environment" >&/dev/null
    export HEADERS=@EMC2_HOME@/include
    export LIBDIR=${TOPDIR}/lib
    export REALTIME=realtime
else
    # Set $EMC2_HOME to $prefix for tests that depend on it
    export SYSTEM_BUILD=1
    export EMC2_HOME=@EMC2_HOME@
    export HEADERS=@includedir@/linuxcnc
    export LIBDIR=@EMC2_HOME@/lib
    export LINUXCNC_EMCSH=@WISH@
    export REALTIME=@REALTIME@
    export SUDO=sudo
fi
export PYTHON_CPPFLAGS="@PYTHON_CPPFLAGS@"
export PYTHON_EXTRA_LIBS="@PYTHON_EXTRA_LIBS@"
export PYTHON_EXTRA_LDFLAGS="@PYTHON_EXTRA_LDFLAGS@"
export PYTHON_LIBS="@PYTHON_EXTRA_LIBS@"

RUNTESTS="$(readlink -f "$0")"
export RUNTESTS

NUM=0
FAIL=0; FAIL_NAMES=""
XFAIL=0
SHMERR=0
SKIP=0
VERBOSE=0

clean () {
    find "$*" \( -name "stderr" -or -name "result" \
	-or -name "*.var" -or -name "*.var.bak" \) \
	-print0 | xargs -0 rm -f
}

wait_for_result_close() {
    # Test for the 'result' and 'stderr' files in the current testdir to be
    # closed. The 'checkresult' script cannot be run if these files are still
    # in use and would cause a race condition.
    # This function should be called with the test's directory as CWD.
    presult="$(realpath -e -q "./result")"
    pstderr="$(realpath -e -q "./stderr")"
    if [ -z "$presult" ] || [ -z "$pstderr" ]; then
        echo "Internal error: Missing 'result' or 'stderr' in wait_for_result_close()"
        exit 2
    fi
    timeoutcnt=0
    while true; do
        lsof -- "$presult" > /dev/null 2>&1; resresult=$?
        lsof -- "$pstderr" > /dev/null 2>&1; resstderr=$?
        if [ $resresult -ne 0 ] && [ $resstderr -ne 0 ]; then
            # Neither 'result' nor 'stderr' are open anymore
            break
        fi
        if [ $timeoutcnt -ge 30 ]; then
            if [ $resresult -eq 0 ]; then
                echo "*** Timeout waiting for 'result' file to close"
            fi
            if [ $resstderr -eq 0 ]; then
                echo "*** Timeout waiting for 'stderr' file to close"
            fi
            echo "*** Test results may be invalid when checked."
            return 1
        fi
        sleep 1
        timeoutcnt=$((timeoutcnt + 1))
    done
    return 0
}

run_shell_script () {
    testname=$(basename "$1")
    testdir=$(dirname "$1")

    pushd "$testdir" > /dev/null || exit 2
    if [ $VERBOSE -eq 1 ]; then
        (bash -x "$testname" | tee result) 3>&1 1>&2 2>&3 | tee stderr
    else
        bash -x "$testname" > result 2> stderr
    fi
    exitcode=$?
    wait_for_result_close
    popd > /dev/null || exit 2
    return "$exitcode"
}

run_executable () {
    testname=$(basename "$1")
    testdir=$(dirname "$1")

    pushd "$testdir" > /dev/null || exit 2
    if [ $VERBOSE -eq 1 ]; then
        (./"$testname" | tee result) 3>&1 1>&2 2>&3 | tee stderr
    else
        ./"$testname" > result 2> stderr
    fi
    exitcode=$?
    wait_for_result_close
    popd > /dev/null || exit 2
    return "$exitcode"
}

run_without_overruns () {
    testname=$(basename "$1")
    testdir=$(dirname "$1")
    for i in $(seq 10); do
        if [ "$i" != 1 ]; then echo "--- $testdir: overrun detected in sampler, re-running test" 1>&2 ; fi

        pushd "$testdir" > /dev/null || exit 2
        if [ $VERBOSE -eq 1 ]; then
            (halrun -f "$testname" | tee result) 3>&1 1>&2 2>&3 | tee stderr
        else
            halrun -f "$testname" > result 2> stderr
        fi
        exitcode=$?
        wait_for_result_close
        popd > /dev/null || exit 2

        if ! grep -q '^overrun$' "$testdir/result"; then return "$exitcode"; fi
    done
    echo "--- $testdir: $i overruns detected, giving up" 1>&2
    return 1
}

run_test() {
    testname="$1"
    case "$testname" in
        *.hal) run_without_overruns "$testname" ;;
        *.sh) run_shell_script "$testname" ;;
        *) run_executable "$testname" ;;
    esac
}

SHMEM_KEY=( "0x00000064" "0x48414c32" "0x48484c34" "0x90280a48" "0x130cf406" "0x434c522b" )
SHMEM_USE=( "Emc motion" "Hal"        "UUID"       "Rtapi"      "Hal scope"  "Classicladder" )
test_shmem() {
    # Test if there are any shared memory segments left. These will
    # interfere with performing tests as we cannot be assured of a clean
    # start for each test if a segment exists.
    mapfile -t keys < <(ipcs -m | grep -Ei "^\\s*($(IFS='|'; echo "${SHMEM_KEY[*]}"))" | awk '{print $1}')
    if [ "${#keys[@]}" -gt 0 ]; then
        echo "There are one or more shared memory segments currently allocated."
        echo "This indicates that there is a LinuxCNC instance running or it"
        echo "did not cleanup before exit."
        echo "You should run 'ipcs -m' and look for the following keys to cleanup:"
        for i in "${!SHMEM_KEY[@]}"; do
            for j in "${keys[@]}"; do
                if [ "$j" = "${SHMEM_KEY[$i]}" ]; then
                    echo "${SHMEM_KEY[$i]} - ${SHMEM_USE[$i]} key"
                    break
                fi
            done
        done
        echo
        echo "You should remove the key(s) with 'ipcrm -M <key>' if LinuxCNC is"
        echo "not running and no processes are attached to the segment(s)."
        return 1
    else
        return 0
    fi
}

test_and_remove_shmem() {
    ret=0
    for i in "${!SHMEM_KEY[@]}"; do
        # ipcs returns the following columns:
        #   key shmid owner perms bytes nattch status
        read -r -a SHM < <(ipcs -m | grep -Ei "^\\s*${SHMEM_KEY[$i]}")
        if [ "${#SHM[@]}" -ge 6 ]; then
            echo "*** SHMERR: Shared memory segment ${SHMEM_KEY[$i]} (${SHMEM_USE[$i]} key) was not removed. Removing..."
            ipcrm -M "${SHMEM_KEY[$i]}"
            # Check number of attached processes. It should be zero.
            if [ "${SHM[5]}" -ne 0 ]; then
                echo "*** SHMERR: Shared memory segment ${SHMEM_KEY[$i]} has at least one attached process."
                echo "*** SHMERR: Manual intervention is required to solve the situation."
                return 2
            fi
            ret=1
        fi
    done
    return $ret
}

TMPDIR=$(mktemp -d /tmp/runtest.XXXXXX)
trap 'rm -rf "$TMPDIR"' 0 1 2 3 15


run_tests () {
    if ! test_shmem; then
        exit 1;
    fi

    find "$@" -name test.hal -or -name test.sh -or -name test \
	| sort > "$TMPDIR/alltests"

    while read -r testname; do
	testdir=$(dirname "$testname")
        # check if there's a "musthave" file with prerequisites from config.h
        if [ -e "$testdir/musthave" ] ; then
            # one prerequisite per line
            while IFS= read -r prereq ; do
                if ! grep --quiet --regexp "^#define HAVE_$prereq.*" "$TOPDIR/src/config.h" ; then
                    echo "Skipping test for missing prerequisite \"$prereq\": $testdir" 1>&2
                    SKIP=$((SKIP + 1))
                    continue 3
                fi
            done < "$testdir/musthave"
        fi
        # skip test if there's a "skip" file
	if [ -e "$testdir/skip" ]; then
	    if ! [ -x "$testdir/skip" ] || ! "$testdir/skip"; then
		echo "Skipping disabled test: $testdir" 1>&2
		SKIP=$((SKIP + 1))
		continue
	    fi
	fi
	if $NOSUDO && [ -e "$testdir/control" ] && \
		grep Restrictions: "$testdir/control" | grep -q sudo; then
	    if ! [ -x "$testdir/skip" ] || ! "$testdir/skip"; then
		echo "Skipping sudo test: $testdir" 1>&2
		SKIP=$((SKIP + 1))
		continue
	    fi
	fi
	NUM=$((NUM + 1))
	TEST_DIR=$(readlink -f "$testdir")
	export TEST_DIR
	echo "Running test: $testdir" 1>&2
        if test -n "$SYSTEM_BUILD"; then
            # Tell `halcompile` where to install comps
            USER_MODULE_DIR=$(readlink -f "$testdir") \
                PATH=$(readlink -f "$testdir"):$PATH \
                run_test "$testname"
        else
            run_test "$testname"
        fi
	exitcode=$?
	if [ "$exitcode" -ne 0 ]; then
	    reason="test run exited with $exitcode"
	else
	    if [ -e "$testdir/checkresult" ]; then
		"$testdir/checkresult" "$testdir/result"
		exitcode=$?
		reason="checkresult exited with $exitcode"
	    elif [ -f "$testdir/expected" ]; then
		cmp -s "$testdir/expected" "$testdir/result"
		exitcode=$?
		reason="result differed from expected"
		if [ "$exitcode" -ne 0 ]; then
		    diff -u "$testdir/expected" "$testdir/result" > "$TMPDIR/diff"
		    SIZE=$(wc -l < "$TMPDIR/diff")
		    if [ "$SIZE" -lt 40 ]; then
			cat "$TMPDIR/diff"
		    else
			OMIT=$((SIZE-40))
			head -40 "$TMPDIR/diff"
			echo "($OMIT more lines omitted)"
		    fi
		fi
	    else
		exitcode=1
		reason="Neither expected nor checkresult existed"
	    fi
	fi
	if [ "$exitcode" -ne 0 ]; then
	    echo "*** $testdir: XFAIL: $reason"
            if test $PRINT = 1; then
                echo "************** result:"
                tail -500 "$testdir/result" | sed 's/^/        /'
                echo "************** stderr:"
                tail -500 "$testdir/stderr" | sed 's/^/        /'
                echo "**************"
            fi
	    if [ -f "$testdir/xfail" ]; then
		XFAIL=$((XFAIL + 1))
		if [ $NOCLEAN -eq 0 ]; then
		    rm -f "$testdir/stderr" "$testdir/result" \
			"$testdir"/*.var "$testdir"/*.var.bak
		fi
	    else
		FAIL=$((FAIL + 1))
		FAIL_NAMES="$FAIL_NAMES"$'\n'"$testdir"
	    fi
            if test $STOP = 1; then
	        break
	    fi
	else
	    if [ -f "$testdir/xfail" ]; then
		echo "*** $testdir: XPASS: Passed, but was expected to fail"
	    else
		if [ $NOCLEAN -eq 0 ]; then
		    rm -f "$testdir/stderr" "$testdir/result" \
			"$testdir"/*.var "$testdir"/*.var.bak
		fi
	    fi
	fi

        if ! test_and_remove_shmem; then
            if [ $? -eq 2 ]; then
                # Cannot remove attached segments. Fail hard.
                exit 1
            fi
            SHMERR=$((SHMERR + 1))
        fi
    done < "$TMPDIR/alltests"

    SUCC=$((NUM-FAIL-XFAIL))
    echo "Runtest: $NUM tests run, $SUCC successful, $FAIL failed + $XFAIL expected, $SKIP skipped, $SHMERR shmem errors"
    if [ "$FAIL" -ne 0 ]; then
	echo "Failed: $FAIL_NAMES"
	exit 1;
    else
	exit 0;
    fi
}

usage () {
    P=${0##*/}
    cat <<EOF
$P: Run HAL test suite items

Usage:
    $P [-n] [-s] [-p] tests
	Run tests.  With '-n', do not remove temporary files for successful
	tests.  With '-s', stop after any failed test.  With '-p', print
        stderr and result files.

    $P -c tests
	Remove temporary files from an earlier test run.

    $P -u
        Only run tests that require normal user access.  Skip tests
        requiring root or sudo.

    $P -v
        Show stdout and stderr (normally it's hidden).
EOF
}

CLEAN_ONLY=0
NOCLEAN=0
NOSUDO=false
STOP=0
PRINT=0
while getopts cnuvsph opt; do
    case "$opt" in
    c) CLEAN_ONLY=1 ;;
    n) NOCLEAN=1 ;;
    u) NOSUDO=true ;;
    v) VERBOSE=1 ;;
    s) STOP=1 ;;
    p) PRINT=1 ;;
    h|?) usage; exit 0 ;;
    *) usage; exit 1 ;;
    esac
done
shift $((OPTIND-1))

if [ $# -eq 0 ]; then
    if [ -f test.hal ] || [ -f test.sh ]; then
        set -- .
    else
        set -- "$TOPDIR/tests"
    fi
fi

if [ $CLEAN_ONLY -eq 1 ]; then
    clean "$@"
else
    run_tests "$@"
fi
